package org.apache.solr.client.solrj.beans;


import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrInputDocument;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Method;
import java.lang.reflect.Array;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * A class to map objects to and from solr documents.
 * 
 * @version $Id: DocumentObjectBinder.java 693132 2008-09-08 15:22:18Z koji $
 * @since solr 1.3
 * 
 * modify:id值增加了类名，类名-idvalue ;方便在多个domain中部署
 */
public class DocumentObjectBinder {
	private final Map<Class, List<DocField>> infocache = new ConcurrentHashMap<Class, List<DocField>>();

	public DocumentObjectBinder() {
	}

	public <T> List<T> getBeans(Class<T> clazz, SolrDocumentList solrDocList) {
		List<DocField> fields = getDocFields(clazz);
		List<T> result = new ArrayList<T>(solrDocList.size());

		for (int j = 0; j < solrDocList.size(); j++) {
			SolrDocument sdoc = solrDocList.get(j);

			T obj = null;
			try {
				obj = clazz.newInstance();
				result.add(obj);
			} catch (Exception e) {
				throw new RuntimeException("Could not instantiate object of "
						+ clazz, e);
			}
			for (int i = 0; i < fields.size(); i++) {
				DocField docField = fields.get(i);
				docField.inject(obj, sdoc);
			}
		}
		return result;
	}

	public SolrInputDocument toSolrInputDocument(Object obj) {
		List<DocField> fields = getDocFields(obj.getClass());
		if (fields.isEmpty()) {
			throw new RuntimeException("class: " + obj.getClass()
					+ " does not define any fields.");
		}

		SolrInputDocument doc = new SolrInputDocument();
		for (DocField field : fields) {
			if (field.name.equalsIgnoreCase("id")) {
				doc.setField(field.name, obj.getClass().getSimpleName()+"-"+field.get(obj), 1.0f);
			} else
				doc.setField(field.name, field.get(obj), 1.0f);
		}
		return doc;
	}

	private List<DocField> getDocFields(Class clazz) {
		List<DocField> fields = infocache.get(clazz);
		if (fields == null) {
			synchronized (infocache) {
				infocache.put(clazz, fields = collectInfo(clazz));
			}
		}
		return fields;
	}

	private List<DocField> collectInfo(Class clazz) {
		List<DocField> fields = new ArrayList<DocField>();
		Class superClazz = clazz;
		ArrayList<AccessibleObject> members = new ArrayList<AccessibleObject>();
		while (superClazz != null && superClazz != Object.class) {
			members.addAll(Arrays.asList(superClazz.getDeclaredFields()));
			members.addAll(Arrays.asList(superClazz.getDeclaredMethods()));
			superClazz = superClazz.getSuperclass();
		}
		for (AccessibleObject member : members) {
			if (member.isAnnotationPresent(Field.class)) {
				member.setAccessible(true);
				fields.add(new DocField(member));
			}
		}
		return fields;
	}

	private static class DocField {
		private String name;
		private java.lang.reflect.Field field;
		private Method setter;
		private Method getter;
		private Class type;
		private boolean isArray = false, isList = false;

		public DocField(AccessibleObject member) {
			if (member instanceof java.lang.reflect.Field) {
				field = (java.lang.reflect.Field) member;
			} else {
				setter = (Method) member;
			}
			Field annotation = member.getAnnotation(Field.class);
			storeName(annotation);
			storeType();

			// Look for a matching getter
			if (setter != null) {
				String gname = setter.getName();
				if (gname.startsWith("set")) {
					gname = "get" + gname.substring(3);
					try {
						getter = setter.getDeclaringClass().getMethod(gname,
								(Class[]) null);
					} catch (Exception ex) {
						// no getter -- don't worry about it...
						if (type == Boolean.class) {
							gname = "is" + setter.getName().substring(3);
							try {
								getter = setter.getDeclaringClass().getMethod(
										gname, (Class[]) null);
							} catch (Exception ex2) {
								// no getter -- don't worry about it...
							}
						}
					}
				}
			}
		}

		private void storeName(Field annotation) {
			if (annotation.value().equals(Field.DEFAULT)) {
				if (field != null) {
					name = field.getName();
				} else {
					String setterName = setter.getName();
					if (setterName.startsWith("set") && setterName.length() > 3) {
						name = setterName.substring(3, 4).toLowerCase()
								+ setterName.substring(4);
					} else {
						name = setter.getName();
					}
				}
			} else {
				name = annotation.value();
			}
		}

		private void storeType() {
			if (field != null) {
				type = field.getType();
			} else {
				Class[] params = setter.getParameterTypes();
				if (params.length != 1)
					throw new RuntimeException(
							"Invalid setter method. Must have one and only one parameter");
				type = params[0];
			}
			if (type == Collection.class || type == List.class
					|| type == ArrayList.class) {
				type = Object.class;
				isList = true;
				/*
				 * ParameterizedType parameterizedType = null; if(field !=null){
				 * if( field.getGenericType() instanceof ParameterizedType){
				 * parameterizedType = (ParameterizedType)
				 * field.getGenericType(); Type[] types =
				 * parameterizedType.getActualTypeArguments(); if (types != null
				 * && types.length > 0) type = (Class) types[0]; } }
				 */
			} else if (type.isArray()) {
				isArray = true;
				type = type.getComponentType();
			}
		}

		public <T> void inject(T obj, SolrDocument sdoc) {
			Object val = sdoc.getFieldValue(name);
			if (val == null)
				return;
			if (isArray) {
				if (val instanceof List) {
					List collection = (List) val;
					set(obj, collection.toArray((Object[]) Array.newInstance(
							type, collection.size())));
				} else {
					Object[] arr = (Object[]) Array.newInstance(type, 1);
					arr[0] = val;
					set(obj, arr);
				}
			} else if (isList) {
				if (val instanceof List) {
					set(obj, val);
				} else {
					ArrayList l = new ArrayList();
					l.add(val);
					set(obj, l);
				}
			} else {
				if (val instanceof List) {
					List l = (List) val;
					if (l.size() > 0)
						set(obj, l.get(0));
				} else {
					set(obj, val);
				}
			}
		}

		private void set(Object obj, Object v) {
			try {
				if (field != null) {
					field.set(obj, v);
				} else if (setter != null) {
					setter.invoke(obj, v);
				}
			} catch (Exception e) {
				throw new RuntimeException("Exception while setting value : "
						+ v + " on " + (field != null ? field : setter), e);
			}
		}

		public Object get(final Object obj) {
			if (field != null) {
				try {
					return field.get(obj);
				} catch (Exception e) {
					throw new RuntimeException(
							"Exception while getting value: " + field, e);
				}
			} else if (getter == null) {
				throw new RuntimeException(
						"Missing getter for field: "
								+ name
								+ " -- You can only call the 'get' for fields that have a field of 'get' method");
			}

			try {
				return getter.invoke(obj, (Object[]) null);
			} catch (Exception e) {
				throw new RuntimeException("Exception while getting value: "
						+ getter, e);
			}
		}
	}
}
